/*
 * Copyright (C) 2007 Christian Laireiter <liree@web.de>
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms 
 * of the GNU General Public License as published by the Free Software Foundation; either 
 * version 2 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied 
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along with 
 *  this program; if not, write to the Free Software Foundation, 
 *  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
 */
package mo.network;

import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import mo.network.client.listeners.IReceiver;
import mo.network.client.listeners.ISender;
import mo.network.server.data.Packet;

/**
 * This {@link NetworkPeer} controls the flow of {@link Packet} objects being
 * send.<br>
 * If a packet for a specific {@linkplain Packet#getSocket() target} has
 * successfully been send, the next queue one for this target will be enqueued
 * to the wrapped {@link NetworkPeer}.<br>
 * If a packet fails, it will be re-send several times.<br>
 * 
 * @author Christian Laireiter
 */
public class BufferedNetworkPeer implements NetworkPeer {

    /**
     * Structure for storing information about an active destination (receiver).
     * 
     * @author Christian Laireiter
     */
    private final class DestinationInfo {
        /**
         * Stores the amount of currently occurred send fails for the current
         * packet.
         */
        private int currentFailCounter = 0;

        /**
         * The target socket.
         */
        private final Socket destination;

        /**
         * Stores the queued packets.
         */
        private final List<Packet> queue;

        /**
         * Creates an instance.
         * 
         * @param socket
         *            represented target socket.
         */
        public DestinationInfo(final Socket socket) {
            assert socket != null;
            this.destination = socket;
            this.queue = new ArrayList<Packet>();
        }

        /**
         * Adds a new packet which is about to be send.
         * 
         * @param toQueue
         *            packet to send.
         */
        public void enqueue(final Packet toQueue) {
            synchronized (this.queue) {
                this.queue.add(toQueue);
            }
        }

        /**
         * (overridden)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(final Object obj) {
            boolean result = false;
            if (obj instanceof DestinationInfo) {
                final DestinationInfo other = (DestinationInfo) obj;
                result = this.destination.equals(other.destination);
            }
            return result;
        }

        /**
         * Increments the {@linkplain #currentFailCounter fail counter}.
         */
        public void failOccured() {
            this.currentFailCounter++;
        }

        /**
         * @return the currentFailCounter
         */
        public int getCurrentFailCounter() {
            return this.currentFailCounter;
        }

        /**
         * Returns the current packet.
         * 
         * @return current packet.
         */
        public Packet getCurrentPacket() {
            Packet result = null;
            synchronized (this.queue) {
                if (!this.queue.isEmpty()) {
                    result = this.queue.get(0);
                }
            }
            return result;
        }

        /**
         * (overridden)
         * 
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return this.destination.hashCode();
        }

        /**
         * Returns <code>true</code> if the packet queue is empty.
         * 
         * @return <code>true</code> if the packet queue is empty.
         */
        public boolean isEmpty() {
            synchronized (this.queue) {
                return this.queue.isEmpty();
            }
        }

        /**
         * Removes the current packet and resets the fail counter.
         */
        public void sendSucceed() {
            synchronized (this.queue) {
                this.queue.remove(0);
                this.currentFailCounter = 0;
            }
        }
    }

    /**
     * Amount of retries, before this peer handles a peer to fail.
     */
    public final static int MAX_FAILS_TILL_FAIL = 50;

    /**
     * Packet queue, ordered by destinations.
     */
    private final Map<Socket, DestinationInfo> destinations;

    /**
     * Send listeners to forward events to.
     */
    private final List<ISender> sendListeners;

    /**
     * Stores the network peer to which the packets are forwarded.
     */
    private final NetworkPeer wrapped;

    /**
     * Creates an instance.
     * 
     * @param toWrap
     *            The network peer to which the packets will be forwarded.
     */
    public BufferedNetworkPeer(final NetworkPeer toWrap) {
        assert toWrap != null;
        this.wrapped = toWrap;
        this.wrapped.addSendListener(new ISender() {

            public void packetFailed(final Packet thePacket) {
                BufferedNetworkPeer.this.packetFailed(thePacket);
            }

            public void packetSent(final Packet thePacket) {
                BufferedNetworkPeer.this.packetSent(thePacket);

            }

        });
        this.sendListeners = new ArrayList<ISender>();
        this.destinations = new HashMap<Socket, DestinationInfo>();
    }

    /**
     * (overridden)
     * 
     * @see mo.network.NetworkPeer#addReceiveListener(mo.network.client.listeners.IReceiver)
     */
    public void addReceiveListener(final IReceiver receiver) {
        this.wrapped.addReceiveListener(receiver);
    }

    /**
     * (overridden)
     * 
     * @see mo.network.NetworkPeer#addSendListener(mo.network.client.listeners.ISender)
     */
    public void addSendListener(final ISender sender) {
        synchronized (this.sendListeners) {
            if (!this.sendListeners.contains(sender)) {
                this.sendListeners.remove(sender);
            }
        }
    }

    /**
     * Returns the {@link DestinationInfo} for the socket of the provided
     * packet.
     * 
     * @param packet
     *            the packet to obtain the {@link DestinationInfo} for.
     * @return The {@link DestinationInfo} for the socket of the packet.
     */
    private DestinationInfo getDestinationInfo(final Packet packet) {
        DestinationInfo result = null;
        synchronized (this.destinations) {
            result = this.destinations.get(packet.getSocket());
            if (result == null) {
                result = new DestinationInfo(packet.getSocket());
                this.destinations.put(packet.getSocket(), result);
            }
        }
        return result;
    }

    /**
     * Handles packet fail events.
     * 
     * @param thePacket
     *            The packet which has failed to send by {@link #wrapped}.
     */
    protected void packetFailed(final Packet thePacket) {
        System.err.println("Packetfail");
        final DestinationInfo destinationInfo = getDestinationInfo(thePacket);
        synchronized (destinationInfo) {
            destinationInfo.failOccured();
            if (destinationInfo.getCurrentFailCounter() == MAX_FAILS_TILL_FAIL) {
                synchronized (this.sendListeners) {
                    for (final ISender current : this.sendListeners) {
                        current.packetFailed(thePacket);
                    }
                }
            } else {
                this.wrapped.queueUpload(new Packet(thePacket.getPayLoad(),
                        thePacket.getSocket()));
            }
        }
    }

    /**
     * Handles packet send events.
     * 
     * @param thePacket
     *            the packet which has successfully been send by
     *            {@link #wrapped}.
     */
    protected void packetSent(final Packet thePacket) {
        final DestinationInfo destinationInfo = getDestinationInfo(thePacket);
        synchronized (destinationInfo) {
            destinationInfo.sendSucceed();
            if (!destinationInfo.isEmpty()) {
                this.wrapped.queueUpload(destinationInfo.getCurrentPacket());
            }
        }
        synchronized (this.sendListeners) {
            for (final ISender current : this.sendListeners) {
                current.packetSent(thePacket);
            }
        }
    }

    /**
     * (overridden)
     * 
     * @see mo.network.NetworkPeer#queueUpload(mo.network.server.data.Packet)
     */
    public void queueUpload(final Packet newPacket) {
        synchronized (this.destinations) {
            final DestinationInfo destinationInfo = getDestinationInfo(newPacket);
            synchronized (destinationInfo) {
                final boolean empty = destinationInfo.isEmpty();
                destinationInfo.enqueue(newPacket);
                if (empty) {
                    this.wrapped.queueUpload(newPacket);
                }
            }
        }
    }

    /**
     * (overridden)
     * 
     * @see mo.network.NetworkPeer#removeReceiveListener(mo.network.client.listeners.IReceiver)
     */
    public void removeReceiveListener(final IReceiver receiver) {
        this.wrapped.removeReceiveListener(receiver);
    }

    /**
     * (overridden)
     * 
     * @see mo.network.NetworkPeer#removeSendListener(mo.network.client.listeners.ISender)
     */
    public void removeSendListener(final ISender sender) {
        synchronized (this.sendListeners) {
            this.sendListeners.remove(sender);
        }
    }
}
